using System;
using System.Collections.Generic;
using System.Text;
using System.Drawing;
using System.Threading;
using System.Runtime.InteropServices;
using DirectShowLib;
using Microsoft.DirectX;
using Microsoft.DirectX.Direct3D;

namespace MultiVMR9cs
{
    public class VideoSource : IDisposable
    {
        #region Fields

        // the MultiVMR9Renderer this source belongs to
        protected MultiVMR9Renderer _parentRenderer;

        // the Allocator that delivers the Video's private textures
        internal Allocator _allocator;

        // the default 3d object to draw our frames of Video
        protected CustomVertex.PositionColoredTextured[] vertices;
        protected VertexBuffer vertexBuffer = null;
        protected const int E_FAIL = unchecked((int)0x80004005);

        // Interfaces for controling DirectShow Sources
        internal IFilterGraph _filterGraph;
        internal IBaseFilter _vmr9;
        internal IMediaControl _mediaControl;
        internal IMediaSeeking _mediaSeeking;
        internal IBasicAudio _basicAudio;
        internal IMediaEventEx _mediaEventEx;

        // the thread that will receive events from the graph
        private Thread _eventThread;
        private ManualResetEvent m_mre;
        internal GraphState _graphState = GraphState.Stopped;

        // list of pin pairs needed to restore VMR9 after a device reset
        private List<ResetPinPair> _resetPinList;

        // the transformation matrix for the 3d object
        protected Matrix _worldMatrix = Matrix.Identity;

        // the start position of the graph
        private long _startPos = 0;

        // flag to indicate if the Source uses z-buffer reads
        private bool _zenable = true;

        // is the source to be drawn?
        private bool _visible = true;

        // transparency for the 3d object
        private float _alpha = 1f;

        // volume for this source
        private float _volume = 1f;

        // SynchronizationContext that is used to marshal method calls from the event thread to the
        // thread that created that this video source
        private SynchronizationContext context;

        // flag to indicate the media should be looped when completed
        private bool _loop = true;

        // World transformation values
        private float _xpos;
        private float _ypos;
        private float _zpos;
        private float _xscale = 1;
        private float _yscale = 1;
        private float _zscale = 1;
        private float _xrot;
        private float _yrot;
        private float _zrot;

        private DsROTEntry _rotentry;

        private bool disposed = false;

        #endregion

        #region ctor

        public VideoSource(MultiVMR9Renderer parentRenderer)
        {
            _parentRenderer = parentRenderer;
        }

        public VideoSource(MultiVMR9Renderer parentRenderer, string filename, bool YUVMixingMode)
        {
            int hr = 0;

            // get this thread's SynchronizationContext
            context = SynchronizationContext.Current;

            // store the renderer this Video source will belong to
            _parentRenderer = parentRenderer;

            _filterGraph = (IFilterGraph)new FilterGraph();
            _vmr9 = (IBaseFilter)new VideoMixingRenderer9();
            IVMRFilterConfig9 filterConfig = (IVMRFilterConfig9)_vmr9;

            // Set to Renderless mode
            hr = filterConfig.SetRenderingMode(VMR9Mode.Renderless);
            DsError.ThrowExceptionForHR(hr);

            // create the allocator
            _allocator = new Allocator(_parentRenderer.Device,YUVMixingMode);

            IVMRSurfaceAllocatorNotify9 vmrSurfAllocNotify = (IVMRSurfaceAllocatorNotify9)_vmr9;

            // Notify the VMR9 filter about our allocator.
            // We'll use the pointer to the IUknown for this object as the dwID
            hr = vmrSurfAllocNotify.AdviseSurfaceAllocator(Marshal.GetIUnknownForObject(this), _allocator);
            DsError.ThrowExceptionForHR(hr);

            // Notify our allocator about the VMR9 filter
            hr = _allocator.AdviseNotify(vmrSurfAllocNotify);
            DsError.ThrowExceptionForHR(hr);

            // Select the mixer mode : YUV or RGB
            if (YUVMixingMode)
            {
                // Set the number of streams to 1
                hr = filterConfig.SetNumberOfStreams(1);
                DsError.ThrowExceptionForHR(hr);

                IVMRMixerControl9 mixerControl = (IVMRMixerControl9)_vmr9;

                // Get the current Mixing Prefs
                VMR9MixerPrefs dwPrefs;
                mixerControl.GetMixingPrefs(out dwPrefs);

                // Remove the current render target flag.
                dwPrefs &= ~VMR9MixerPrefs.RenderTargetMask;

                // Add the render target flag that we want.
                dwPrefs |= VMR9MixerPrefs.RenderTargetYUV;

                hr = mixerControl.SetMixingPrefs(dwPrefs);
                DsError.ThrowExceptionForHR(hr);
            }

            // Add the filter to the graph
            hr = _filterGraph.AddFilter(_vmr9, "Video Mixing Renderer 9");
            DsError.ThrowExceptionForHR(hr);

            // Render the file
            hr = (_filterGraph as IGraphBuilder).RenderFile(filename, null);
            DsError.ThrowExceptionForHR(hr);

            // Get the interfaces needed to control the graph
            _mediaControl = _filterGraph as IMediaControl;
            _mediaEventEx = _filterGraph as IMediaEventEx;
            _mediaSeeking = _filterGraph as IMediaSeeking;
            _basicAudio = _filterGraph as IBasicAudio;

            // Get the event handle the graph will use to signal
            // when events occur
            IntPtr hEvent;
            hr = _mediaEventEx.GetEventHandle(out hEvent);

            // Wrap the graph event with a ManualResetEvent
            m_mre = new ManualResetEvent(false);
            m_mre.Handle = hEvent;

            // Set the volume
            Volume = 1f;
           
            // Create a new thread to wait for events
            _eventThread = new Thread(new ThreadStart(this.EventWait));
            _eventThread.Name = "Media Event Thread";
            _eventThread.IsBackground = true;
            _eventThread.Start();

            InitVertices();

#if DEBUG
            _rotentry = new DsROTEntry(_filterGraph);
#endif
        }

        protected void InitVertices()
        {
            // create the vertices that will be used to render the Video Texture
            vertices = new CustomVertex.PositionColoredTextured[4];

            // widen the vertices to fit the aspect ratio of the back buffer
            float aspectRatio = (float)_parentRenderer.BackBufferSize.Width / (float)_parentRenderer.BackBufferSize.Height;
            float aradjust = (float)Math.Abs((1f - aspectRatio));

            // center the vertices on the BackBuffer's texels
            float xoff = .5f / (float)_parentRenderer.BackBufferSize.Width;
            float yoff = .5f / (float)_parentRenderer.BackBufferSize.Height;

            vertices[0].Position = new Vector3((-1f - aradjust) - xoff, 1.0f + yoff, 0f); // top left
            vertices[1].Position = new Vector3((-1f - aradjust) - xoff, -1.0f + yoff, 0f); // bottom left
            vertices[2].Position = new Vector3((1f + aradjust) - xoff, 1.0f + yoff, 0f); // top right
            vertices[3].Position = new Vector3((1f + aradjust) - xoff, -1.0f + yoff, 0f); // bottom right

            vertices[0].Color = Color.FromArgb(255, Color.White).ToArgb(); // low left
            vertices[1].Color = Color.FromArgb(255, Color.White).ToArgb(); // high left
            vertices[2].Color = Color.FromArgb(255, Color.White).ToArgb(); // low right
            vertices[3].Color = Color.FromArgb(255, Color.White).ToArgb(); // high right

            // Adjust uv coords for video texture texel center offsets
            float uoff = 0.5f / (float)TextureSize.Width;
            float voff = 0.5f / (float)TextureSize.Height;

            vertices[0].Tu = uoff; vertices[0].Tv = voff; // low left
            vertices[1].Tu = uoff; vertices[1].Tv = 1.0f + voff; // high left
            vertices[2].Tu = 1.0f + uoff; vertices[2].Tv = voff; // low right
            vertices[3].Tu = 1.0f + uoff; vertices[3].Tv = 1.0f + voff; // high right

            vertexBuffer = new VertexBuffer(
              typeof(CustomVertex.PositionColoredTextured),
              4,
              _parentRenderer.Device,
              Usage.WriteOnly,
              CustomVertex.PositionColoredTextured.Format,
              Pool.Managed
              );
        }

        #endregion

        #region Events

        /// <summary>
        /// Event that is raised when a graph is set to the Running state
        /// </summary>
        public event EventHandler GraphRun;

        /// <summary>
        /// Event that is raised when a graph is set to the Paused state
        /// </summary>
        public event EventHandler GraphPaused;

        /// <summary>
        /// Event that is raised when a graph is set to the Stopped state
        /// </summary>
        public event EventHandler GraphStopped;

        #endregion

        #region Properties

        public virtual Texture Texture
        {
            get
            {
                if (_allocator != null)
                {
                    return _allocator.Texture;
                }
                else
                {
                    return null;
                }
            }
        }

        public bool ZBufferEnabled
        {
            get
            {
                return _zenable;
            }
            set
            {
                _zenable = value;
            }
        }

        public bool Loop
        {
            get
            {
                return _loop;
            }
            set
            {
                _loop = value;
            }
        }

        public bool Visible
        {
            get
            {
                return _visible;
            }
            set
            {
                _visible = value;
            }
        }

        public virtual bool KeepAspectRatio
        {
            get
            {
                if (_allocator != null)
                {
                    return _allocator.KeepAspectRatio;
                }
                else
                {
                    return false;
                }
            }
            set
            {
                if (_allocator != null)
                {
                    _allocator.KeepAspectRatio = value;
                }
            }
        }

        public float Volume
        {
            get
            {
                return _volume;
            }
            set
            {
                _volume = Math.Min(1f, Math.Max(0f, value));
                setVolume(_volume * _parentRenderer.MasterVolume);
            }
        }

        public virtual Size TextureSize
        {
            get
            {
                return _allocator.TextureSize;
            }
        }

        public float XPosition
        {
            get
            {
                return _xpos;
            }
            set
            {
                _xpos = value;
                UpdateWorldMatrix();
            }
        }

        public float YPosition
        {
            get
            {
                return _ypos;
            }
            set
            {
                _ypos = value;
                UpdateWorldMatrix();
            }
        }

        public float ZPosition
        {
            get
            {
                return _zpos;
            }
            set
            {
                _zpos = value;
                UpdateWorldMatrix();
            }
        }

        public float XScale
        {
            get
            {
                return _xscale;
            }
            set
            {
                _xscale = value;
                UpdateWorldMatrix();
            }
        }

        public float YScale
        {
            get
            {
                return _yscale;
            }
            set
            {
                _yscale = value;
                UpdateWorldMatrix();
            }
        }

        public float ZScale
        {
            get
            {
                return _zscale;
            }
            set
            {
                _zscale = value;
                UpdateWorldMatrix();
            }
        }

        public float XRotation
        {
            get
            {
                return _xrot;
            }
            set
            {
                _xrot = value;
                UpdateWorldMatrix();
            }
        }

        public float YRotation
        {
            get
            {
                return _yrot;
            }
            set
            {
                _yrot = value;
                UpdateWorldMatrix();
            }
        }

        public float ZRotation
        {
            get
            {
                return _zrot;
            }
            set
            {
                _zrot = value;
                UpdateWorldMatrix();
            }
        }

        /// <summary>
        /// Gets or sets the World transformation matrix for the 3d object
        /// </summary>
        public virtual Matrix WorldMatrix
        {
            get
            {
                return _worldMatrix;
            }
            set
            {
                _worldMatrix = value;
            }
        }

        /// <summary>
        /// Gets or sets the Order in which this Video Source is rendered in relation to it's sibling sources
        /// </summary>
        public int ZOrder
        {
            get
            {
                return _parentRenderer.GetZOrder(this);
            }
            set
            {
                _parentRenderer.SetZOrder(this, value);
            }
        }

        /// <summary>
        /// Gets the MultiVMR9Renderer this Video Source belongs to
        /// </summary>
        public MultiVMR9Renderer ParentRenderer
        {
            get
            {
                return _parentRenderer;
            }
        }

        /// <summary>
        /// Gets the Duration of the Video Source
        /// </summary>
        public long Duration
        {
            get
            {
                long d = 0;
                if (_mediaSeeking != null)
                {
                    _mediaSeeking.GetDuration(out d);
                }
                return d;
            }
        }

        /// <summary>
        /// Gets or sets the Starting position of the Video Source
        /// </summary>
        public long StartPosition
        {
            get
            {
                return _startPos;
            }
            set
            {
                if (value > CurrentPosition)
                {
                    CurrentPosition = value;
                }
                else
                {
                    _startPos = value;
                }
            }
        }

        /// <summary>
        /// Gets or sets the ending position of the Video Source
        /// </summary>
        public long EndPosition
        {
            get
            {
                long c = 0;
                long e = 0;
                if (_mediaSeeking != null)
                {
                    _mediaSeeking.GetPositions(out c, out e);
                }
                return e;
            }
            set
            {
                if (value < CurrentPosition)
                {
                    _mediaSeeking.SetPositions(0, AMSeekingSeekingFlags.AbsolutePositioning, value, AMSeekingSeekingFlags.AbsolutePositioning);

                }
                else
                {
                    _mediaSeeking.SetPositions(0, AMSeekingSeekingFlags.NoPositioning, value, AMSeekingSeekingFlags.AbsolutePositioning);

                }
            }
        }

        /// <summary>
        /// Gets or sets the current playback position of the Video Source
        /// </summary>
        public long CurrentPosition
        {
            get
            {
                long p = 0;
                if (_mediaSeeking != null)
                {
                    _mediaSeeking.GetCurrentPosition(out p);
                }
                return p;
            }
            set
            {
                if (_mediaSeeking != null)
                {
                    _mediaSeeking.SetPositions(value, AMSeekingSeekingFlags.AbsolutePositioning, 0, AMSeekingSeekingFlags.NoPositioning);
                }
            }
        }

        /// <summary>
        /// Gets or sets the Alpha of the Video source
        /// </summary>
        public float Alpha
        {
            get
            {
                return _alpha;
            }
            set
            {
                _alpha = value;
            }
        }

        public virtual Color BorderColor
        {
            get
            {
                if (_allocator != null)
                {
                    return _allocator.BorderColor;
                }
                else
                {
                    return Color.Black;
                }
            }
            set
            {
                if (_allocator != null)
                {
                    _allocator.BorderColor = value;
                }
            }
        }
        #endregion

        #region Public Methods

        public void Run()
        {
            if (_mediaControl != null && _graphState != GraphState.Running)
            {
                _mediaControl.Run();
                _graphState = GraphState.Running;
                if (GraphRun != null)
                {
                    GraphRun(this, new EventArgs());
                }
            }
        }

        public void Pause()
        {
            if (_mediaControl != null && _graphState != GraphState.Paused)
            {
                _mediaControl.Pause();
                _graphState = GraphState.Paused;
                if (GraphPaused != null)
                {
                    GraphPaused(this, new EventArgs());
                }
            }
        }

        public void Stop()
        {
            if (_mediaControl != null && _graphState != GraphState.Stopped)
            {
                _mediaControl.Stop();
                _graphState = GraphState.Stopped;
                if (GraphStopped != null)
                {
                    GraphStopped(this, new EventArgs());
                }
            }
        }

        #endregion

        #region Private Methods

        private void UpdateWorldMatrix()
        {
            _worldMatrix =  Matrix.Translation(_xpos, _ypos, _zpos) * 
                            Matrix.Scaling(_xscale, _yscale, _zscale) * 
                            Matrix.RotationYawPitchRoll(_yrot * 6.28318f, _xrot * 6.28318f, _zrot * 6.28318f);
        }

        #endregion

        #region Internal Methods

        internal virtual void OnAspectRatioChanged(Size newSize)
        {
            // recreate the vertices
            vertexBuffer.Dispose();
            InitVertices();
        }

        /// <summary>
        /// Sets the volume of the Video Source depending it's volume percent and the parent Renderer's volume percent
        /// </summary>
        /// <param name="percent"></param>
        internal void setVolume(float percent)
        {
            if (_basicAudio != null)
            {
                if (percent == 0f)
                {
                    // just mute it
                    _basicAudio.put_Volume(-10000);
                }
                else
                {
                    int lv = (int)(5000f * percent);
                    _basicAudio.put_Volume(lv - 5000);
                }
            }
        }

        #endregion

        #region EventWait Thread

        // Wait for events to happen.  Setting the MediaElement's NotifyWindow prevents it from
        // ever running, so we'll use the EventWait method instead.
        private void EventWait()
        {
            // Returned when GetEvent is called but there are no events
            const int E_ABORT = unchecked((int)0x80004004);

            int hr;
            IntPtr p1, p2;
            EventCode ec;

            do
            {
                // Wait for an event
                m_mre.WaitOne(-1, true);

                // Avoid contention for _graphState
                lock (this)
                {
                    // If we are not shutting down
                    if (_graphState != GraphState.Exiting)
                    {
                        // Read the events
                        for (
                            hr = _mediaEventEx.GetEvent(out ec, out p1, out p2, 0);
                            hr >= 0;
                            hr = _mediaEventEx.GetEvent(out ec, out p1, out p2, 0)
                            )
                        {
                            Console.WriteLine(ec.ToString());
                            switch (ec)
                            {
                                case EventCode.Complete:
                                    {
                                        CurrentPosition = StartPosition;

                                        // do we need to loop when a segment is completed?
                                        if (!_loop)
                                        {
                                            // Call the Stop method on the thread that this Video Source was created on
                                            context.Post(new SendOrPostCallback(delegate(object state)
                                            {
                                                Stop();
                                            }), null);                                          
                                        }
                                        else
                                        {
                                            // Call the Run method on the thread that this Video Source was created on
                                            context.Post(new SendOrPostCallback(delegate(object state)
                                            {
                                                Run();
                                            }), null);  
                                        }
                                    }
                                    break;
                            }
                            // Release any resources the message allocated
                            try
                            {
                                hr = _mediaEventEx.FreeEventParams(ec, p1, p2);
                            }
                            catch (Exception) { }
                            DsError.ThrowExceptionForHR(hr);
                        }

                        // If the error that exited the loop wasn't due to running out of events
                        if (hr != E_ABORT)
                        {
                            DsError.ThrowExceptionForHR(hr);
                        }
                    }
                    else
                    {
                        // We are shutting down
                        break;
                    }
                }
            } while (true);
        }
        #endregion

        #region Virtual Methods

        /// <summary>
        /// Override to clean up any resources created in the default pool.  Make sure to call base.OnDeviceLost()
        /// </summary>
        public virtual void OnDeviceLost()
        {
            _resetPinList = new List<ResetPinPair>();

            // Enumerate the pins in the vmr9.  If they are connected, disconnect them and 
            // add them to the reset list.  This will cause the VMR9 to free any resources it created 
            // on the device.

            IEnumPins enumPins;
            _vmr9.EnumPins(out enumPins);
            IPin[] pins = new IPin[1];
            while (enumPins.Next(pins.Length, pins, IntPtr.Zero) == 0)
            {
                IPin conto = null;
                pins[0].ConnectedTo(out conto);
                if (conto != null)
                {
                    _resetPinList.Add(new ResetPinPair(conto, pins[0]));
                    pins[0].Disconnect();
                    conto.Disconnect();
                }
                else
                {
                    Marshal.ReleaseComObject(pins[0]);
                }
            }
            Marshal.ReleaseComObject(enumPins);

            // delete the surfaces we created in the Allocator
            _allocator.DeleteSurfaces();
        }

        /// <summary>
        /// Override to restore any resources created in the default pool.  Make sure to call base.OnDeviceReset()
        /// </summary>
        public virtual void OnDeviceReset()
        {
            // restore the pin connections on the VMR9
            foreach (ResetPinPair pins in _resetPinList)
            {
                pins.outpin.Connect(pins.inpin, null);
                pins.ReleasePins();
            }
            _resetPinList = null;
        }

        /// <summary>
        /// Called when a device is destroyed and recreated.  (Adapter changed)
        /// Override to detroy and recreate ALL resources created on a DirectX Device.
        /// </summary>
        public virtual void OnDeviceRecreated(Device newDevice)
        {
            // tell the allocator
            if (_allocator != null)
            {
                _allocator.Device = newDevice;
            }

            vertexBuffer.Dispose();
            InitVertices();
        }

        /// <summary>
        /// Override to set up any states needed before a frame is rendered.
        /// </summary>
        /// <returns></returns>
        public virtual int OnCompose()
        {
            int alpha = (int)(255f * _alpha);
            vertices[0].Color = Color.FromArgb(alpha, Color.White).ToArgb(); // low left
            vertices[1].Color = Color.FromArgb(alpha, Color.White).ToArgb(); // high left
            vertices[2].Color = Color.FromArgb(alpha, Color.White).ToArgb(); // low right
            vertices[3].Color = Color.FromArgb(alpha, Color.White).ToArgb(); // high right

            return 0;
        }

        /// <summary>
        /// Override to perform custom drawing of a frame.  Device.BeginScene() will have already been called.
        /// </summary>
        /// <returns></returns>
        public virtual int OnDraw()
        {
            return DrawDefaultMesh(_allocator.Texture);
        }

        protected int DrawDefaultMesh(Texture texture)
        {
            if (_visible)
            {
                if (vertexBuffer == null)
                    return E_FAIL;

                // write the new vertex information into the buffer
                vertexBuffer.SetData(vertices, 0, LockFlags.None);

                _parentRenderer.Device.SamplerState[0].AddressU = TextureAddress.Clamp;
                _parentRenderer.Device.SamplerState[0].AddressV = TextureAddress.Clamp;
                _parentRenderer.Device.SamplerState[0].MagFilter = TextureFilter.Linear;
                _parentRenderer.Device.SamplerState[0].MinFilter = TextureFilter.Linear;
                _parentRenderer.Device.SamplerState[0].MipFilter = TextureFilter.Linear;                

                _parentRenderer.Device.SetTextureStageState(0, TextureStageStates.AlphaOperation, (int)TextureOperation.Modulate);
                _parentRenderer.Device.SetTextureStageState(0, TextureStageStates.AlphaArgument1, (int)TextureArgument.TextureColor);
                _parentRenderer.Device.SetTextureStageState(0, TextureStageStates.AlphaArgument2, (int)TextureArgument.Diffuse);
                _parentRenderer.Device.SetTextureStageState(0, TextureStageStates.ColorArgument1, (int)TextureArgument.TextureColor);

                if (texture != null)
                {
                    _parentRenderer.Device.SetTexture(0, texture);
                }

                _parentRenderer.Device.SetStreamSource(0, vertexBuffer, 0);
                _parentRenderer.Device.VertexFormat = CustomVertex.PositionColoredTextured.Format;

                // alter zenable?
                if (_parentRenderer.Device.RenderState.ZBufferEnable != ZBufferEnabled)
                {
                    _parentRenderer.Device.RenderState.ZBufferEnable = ZBufferEnabled;
                }

                // set the world matrix
                _parentRenderer.Device.Transform.World = WorldMatrix;

                _parentRenderer.Device.DrawPrimitives(PrimitiveType.TriangleStrip, 0, 2);

                if (texture != null)
                {
                    _parentRenderer.Device.SetTexture(0, null);
                }

                return 0;
            }
            else
            {
                return 0;
            }
        }

        #endregion

        #region IDisposable

        public virtual void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        public virtual void Dispose(bool disposing)
        {
            if (!disposed)
            {
                if (_rotentry != null)
                {
                    _rotentry.Dispose();
                }

                // make sure the graph is stopped
                Stop();

                if (_allocator != null)
                {
                    _allocator.Dispose();
                }

                if (_filterGraph != null)
                {
                    Marshal.ReleaseComObject(_filterGraph);
                }

                if (_vmr9 != null)
                {
                    Marshal.ReleaseComObject(_vmr9);
                }

                disposed = true;
            }
        }

        #endregion
    }

    internal class ResetPinPair
    {
        public IPin outpin;
        public IPin inpin;

        public ResetPinPair(IPin OutPin, IPin InPin)
        {
            outpin = OutPin;
            inpin = InPin;
        }

        public void ReleasePins()
        {
            if (outpin != null)
            {
                Marshal.ReleaseComObject(outpin);
                outpin = null;
            }

            if (inpin != null)
            {
                Marshal.ReleaseComObject(inpin);
                inpin = null;
            }
        }
    }

    public enum GraphState
    {
        Stopped,
        Paused,
        Running,
        Exiting
    }
}
